Reading and Processing JSON Responses in Flutter
Flutter applications frequently communicate with REST APIs and web services. These APIs commonly return data in JSON (JavaScript Object Notation) format. Reading and processing JSON responses means receiving JSON text from an API, decoding it into Dart objects, validating the response, and converting the data into model classes that can be safely used in the Flutter application.
Flutter and Dart provide built-in JSON support through the dart:convert library. The main functions used are jsonDecode() for reading JSON and jsonEncode() for converting Dart data into JSON.
1. What is JSON?
JSON stands for JavaScript Object Notation. It is a lightweight text-based format used to represent structured data. JSON is widely used for communication between Flutter applications and backend servers.
{
"id": 101,
"name": "John",
"email": "[email protected]",
"isActive": true
}
2. Why JSON is Used in Flutter
- It is commonly returned by REST APIs.
- It is easy for humans to read and understand.
- It can represent objects, arrays, strings, numbers, booleans, and null values.
- It can be converted into Dart maps and lists.
- It can be converted into strongly typed Dart model classes.
- It is useful for sending and receiving data over HTTP.
3. JSON Data Types
| JSON Type |
Example |
Dart Representation |
| String |
"Flutter" |
String |
| Number |
100 |
int/num |
| Decimal |
99.50 |
double/num |
| Boolean |
true |
bool |
| Null |
null |
null |
| Object |
{"name":"John"} |
Map |
| Array |
[1,2,3] |
List |
4. Importing dart:convert
The dart:convert library provides JSON encoding and decoding functionality.
import 'dart:convert';
5. What is JSON Decoding?
JSON decoding converts a JSON string received from a server into Dart data structures such as Map and List. The jsonDecode() function parses a JSON string and returns the corresponding Dart representation. A JSON object normally becomes a map, while a JSON array becomes a list.
Example JSON Response
const jsonString = '''
{
"id": 1,
"name": "Alice",
"email": "[email protected]"
}
''';
Decoding the JSON
final data = jsonDecode(jsonString);
print(data);
The result is a Dart map containing the JSON properties.
6. Decoding JSON into Map
For a JSON object, it is common to cast the decoded value to Map.
import 'dart:convert';
const jsonString = '''
{
"id": 1,
"name": "Alice",
"email": "[email protected]"
}
''';
final Map data = jsonDecode(jsonString) as Map;
print(data['id']);
print(data['name']);
print(data['email']);
7. Accessing Values from JSON
After decoding an object, individual values can be accessed using their keys.
final Map user = jsonDecode(jsonString) as Map;
final String name = user['name'] as String;
final String email = user['email'] as String;
print(name);
print(email);
8. Type Casting JSON Values
For safer code, JSON values can be explicitly cast to their expected types.
final Map user = jsonDecode(jsonString) as Map;
final int id = user['id'] as int;
final String name = user['name'] as String;
final String email = user['email'] as String;
print('$id - $name - $email');
9. Reading JSON Arrays
A JSON array is represented using square brackets and becomes a Dart List after decoding.
const jsonString = '''
[
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
},
{
"id": 3,
"name": "Charlie"
}
]
''';
final List users = jsonDecode(jsonString) as List;
print(users[0]);
print(users[1]);
print(users[2]);
10. Processing a JSON List
You can loop through a decoded JSON list to process every object.
final List users = jsonDecode(jsonString) as List;
for (final user in users) {
print(user['name']);
}
11. Converting JSON List into a Typed List
For better type safety, JSON objects can be converted into a list of model objects.
final List jsonList = jsonDecode(jsonString) as List;
final List<User> users = jsonList
.map((item) => User.fromJson(item as Map))
.toList();
12. JSON Object vs JSON Array
| JSON Structure |
Dart Result |
{"name":"John"} |
Map |
[{"name":"John"},{"name":"Jane"}] |
List |
"Flutter" |
String |
100 |
num/int |
true |
bool |
null |
null |
13. Reading JSON Responses from an HTTP API
When a Flutter application makes an HTTP request, the server commonly returns an http.Response. The response body is a string. If that body contains JSON, it can be decoded using jsonDecode(). A typical workflow is to check the HTTP status code, decode the response body, and then convert it into a model object.
Required Package
dependencies:
flutter:
sdk: flutter
http: ^1.0.0
Then import the required packages:
import 'dart:convert';
import 'package:http/http.dart' as http;
14. Simple GET Request and JSON Processing
Future<void> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/user/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map;
print(data['name']);
print(data['email']);
} else {
throw Exception('Failed to load user');
}
}
15. Understanding response.body
The response.body property contains the server response as text. If the server sends JSON, it is usually decoded before accessing individual fields.
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
print(response.body);
If the response is JSON, process it with:
final data = jsonDecode(response.body);
16. Checking the HTTP Status Code
Always check the HTTP status code before processing a response as a successful result.
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data);
} else {
throw Exception(
'Request failed with status: ${response.statusCode}',
);
}
17. Common HTTP Status Codes
| Status Code |
Meaning |
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Internal Server Error |
18. Creating a Dart Model Class
Using raw maps everywhere can make a larger application difficult to maintain. A model class provides a structured representation of API data and gives better type safety and autocomplete.
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
}
19. Creating a fromJson() Method
A fromJson() factory constructor can convert a decoded JSON map into a Dart object.
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
20. Processing JSON into a Model Object
final Map jsonMap = jsonDecode(response.body) as Map;
final User user = User.fromJson(jsonMap);
print(user.name);
print(user.email);
21. Why Use Model Classes?
- Improves type safety.
- Makes application code easier to understand.
- Provides IDE autocomplete.
- Centralizes JSON parsing logic.
- Reduces repeated map access.
- Makes API data easier to pass between widgets and services.
- Makes complex nested responses easier to manage.
22. Complete GET Request with Model Processing
import 'dart:convert';
import 'package:http/http.dart' as http;
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/user/1'),
);
if (response.statusCode == 200) {
final jsonMap = jsonDecode(response.body) as Map;
return User.fromJson(jsonMap);
}
throw Exception('Failed to load user');
}
23. Processing a JSON List into Model Objects
Many APIs return an array of objects rather than a single object.
Future<List<User>> fetchUsers() async {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode == 200) {
final List jsonList = jsonDecode(response.body) as List;
return jsonList
.map(
(item) => User.fromJson(item as Map),
)
.toList();
}
throw Exception('Failed to load users');
}
24. Processing Nested JSON
API responses can contain objects inside other objects.
{
"id": 1,
"name": "John",
"address": {
"city": "Mumbai",
"country": "India"
}
}
Address Model
class Address {
final String city;
final String country;
Address({
required this.city,
required this.country,
});
factory Address.fromJson(Map json) {
return Address(
city: json['city'] as String,
country: json['country'] as String,
);
}
}
User Model with Nested Address
class User {
final int id;
final String name;
final Address address;
User({
required this.id,
required this.name,
required this.address,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: Address.fromJson(
json['address'] as Map,
),
);
}
}
25. Processing Nested JSON Arrays
Consider an API response containing a user and a list of orders.
{
"id": 1,
"name": "John",
"orders": [
{
"id": 101,
"product": "Laptop"
},
{
"id": 102,
"product": "Phone"
}
]
}
Order Model
class Order {
final int id;
final String product;
Order({
required this.id,
required this.product,
});
factory Order.fromJson(Map json) {
return Order(
id: json['id'] as int,
product: json['product'] as String,
);
}
}
User Model
class User {
final int id;
final String name;
final List<Order> orders;
User({
required this.id,
required this.name,
required this.orders,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
orders: (json['orders'] as List)
.map(
(item) => Order.fromJson(
item as Map,
),
)
.toList(),
);
}
}
26. Handling Nullable JSON Fields
API fields may sometimes be null. Your Dart model should reflect that possibility.
{
"id": 1,
"name": "John",
"phone": null
}
The corresponding model can use a nullable type:
class User {
final int id;
final String name;
final String? phone;
User({
required this.id,
required this.name,
this.phone,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
);
}
}
27. Handling Missing JSON Fields
Sometimes an API may not return an optional field. You can provide a default value when appropriate.
final String name = json['name'] as String? ?? 'Unknown';
For example:
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String? ?? 'Unknown',
email: json['email'] as String? ?? '',
);
}
28. Handling Invalid JSON
If the response body is not valid JSON, JSON decoding can throw a FormatException.
try {
final data = jsonDecode(response.body);
print(data);
} on FormatException {
print('Invalid JSON response');
}
29. Complete Error Handling
Future<User> fetchUser() async {
try {
final response = await http.get(
Uri.parse('https://example.com/api/user/1'),
);
if (response.statusCode != 200) {
throw Exception(
'Server returned ${response.statusCode}',
);
}
final data = jsonDecode(response.body) as Map;
return User.fromJson(data);
} on FormatException {
throw Exception('Invalid JSON received from server');
} catch (e) {
throw Exception('Failed to process response: $e');
}
}
30. Using JSON with FutureBuilder
Because HTTP requests are asynchronous, Flutter applications can use FutureBuilder to display loading, success, and error states.
FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('No user found');
}
final user = snapshot.data!;
return Text(
'${user.name} - ${user.email}',
);
},
)
31. Reading JSON and Displaying It in a ListView
FutureBuilder<List<User>>(
future: fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final users = snapshot.data ?? [];
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
},
)
32. JSON Encoding
JSON encoding is the opposite of JSON decoding. It converts a Dart object such as a map or list into a JSON string. Dart provides jsonEncode() for this purpose.
final user = {
'name': 'John',
'email': '[email protected]',
};
final jsonString = jsonEncode(user);
print(jsonString);
33. Sending JSON in a POST Request
When sending JSON to a server, convert the Dart map to a JSON string using jsonEncode() and set the appropriate content type.
Future<void> createUser() async {
final user = {
'name': 'John',
'email': '[email protected]',
};
final response = await http.post(
Uri.parse('https://example.com/api/users'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(user),
);
if (response.statusCode == 201) {
print('User created successfully');
} else {
print('Failed to create user');
}
}
34. Adding toJson() to a Model
A model can provide a toJson() method to convert itself into a JSON-compatible map.
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
35. Encoding a Model Object
After defining toJson(), the model can be converted into JSON.
final user = User(
id: 1,
name: 'John',
email: '[email protected]',
);
final jsonString = jsonEncode(user.toJson());
print(jsonString);
36. Complete JSON Processing Flow
- Send an HTTP request.
- Receive the HTTP response.
- Check the HTTP status code.
- Read
response.body.
- Decode the JSON using
jsonDecode().
- Convert the decoded map into a model using
fromJson().
- Use the model object inside the Flutter UI.
HTTP Request
↓
Server Response
↓
response.body
↓
jsonDecode()
↓
Map / List
↓
fromJson()
↓
Dart Model
↓
Flutter UI
37. Manual JSON Processing
For small applications, JSON can be processed manually using jsonDecode() and map access.
final data = jsonDecode(response.body) as Map;
print(data['name']);
print(data['email']);
This approach is simple and requires no additional serialization package.
38. Model-Based JSON Processing
For larger applications, model classes provide a cleaner structure.
final data = jsonDecode(response.body) as Map;
final user = User.fromJson(data);
print(user.name);
print(user.email);
This approach keeps JSON parsing logic inside the model and reduces repeated map access in application code.
39. Manual vs Model-Based Processing
| Feature |
Manual Map |
Model Class |
| Easy for small examples |
Yes |
Yes |
| Type safety |
Limited |
Better |
| Autocomplete |
Limited |
Better |
| Reusable parsing |
Limited |
Yes |
| Nested data |
Can become difficult |
Easier to organize |
| Large application |
Harder to maintain |
More maintainable |
40. Processing API Responses with Separate Service Classes
In a larger Flutter application, API communication can be separated from UI code.
class UserService {
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/user/1'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load user');
}
final data = jsonDecode(response.body) as Map;
return User.fromJson(data);
}
}
41. Service, Model, and UI Architecture
Flutter UI
↓
Service / Repository
↓
HTTP Request
↓
API Server
↓
JSON Response
↓
jsonDecode()
↓
Model.fromJson()
↓
Model Object
↓
Flutter UI
42. Handling API Response Objects with a Data Wrapper
Some APIs wrap their actual data inside another object.
{
"success": true,
"message": "Users fetched successfully",
"data": [
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Jane"
}
]
}
The data can first be extracted from the wrapper.
final json = jsonDecode(response.body) as Map;
final bool success = json['success'] as bool;
final String message = json['message'] as String;
final List data = json['data'] as List;
print(success);
print(message);
print(data.length);
43. Processing API Data Safely
When working with external APIs, do not assume that every field will always exist or have the expected value. Validate important fields and handle missing or invalid data appropriately.
final data = jsonDecode(response.body) as Map;
final name = data['name'];
if (name is String) {
print(name);
} else {
print('Invalid name value');
}
44. JSON Response Error Object
An API may return JSON even when the request fails.
{
"success": false,
"message": "Invalid credentials",
"code": "AUTH_ERROR"
}
You should check the HTTP status and, when appropriate, inspect the JSON error payload.
if (response.statusCode != 200) {
final errorData = jsonDecode(response.body) as Map;
final message =
errorData['message'] as String? ?? 'Unknown error';
throw Exception(message);
}
45. Handling Empty Responses
Some endpoints may return an empty body, especially for operations that do not return data. Avoid blindly calling jsonDecode() on an empty response.
if (response.body.isNotEmpty) {
final data = jsonDecode(response.body);
print(data);
} else {
print('Response body is empty');
}
46. JSON Field Naming
API field names may use naming conventions such as snake_case while Dart properties commonly use camelCase.
{
"first_name": "John",
"last_name": "Smith",
"profile_image": "profile.jpg"
}
The model can map these API fields manually:
class User {
final String firstName;
final String lastName;
final String profileImage;
User({
required this.firstName,
required this.lastName,
required this.profileImage,
});
factory User.fromJson(Map json) {
return User(
firstName: json['first_name'] as String,
lastName: json['last_name'] as String,
profileImage: json['profile_image'] as String,
);
}
}
47. Large JSON Responses
Large JSON responses can require more processing time and memory. For large datasets or files, JSON parsing can be moved away from the main UI work when appropriate so that the interface remains responsive.
48. Using compute() for Heavy JSON Parsing
For CPU-intensive parsing, Flutter applications can move parsing work to another isolate using appropriate asynchronous techniques.
import 'dart:convert';
import 'package:flutter/foundation.dart';
List<User> parseUsers(String responseBody) {
final List jsonList = jsonDecode(responseBody) as List;
return jsonList
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
}
Future<List<User>> parseUsersInBackground(
String responseBody,
) async {
return compute(parseUsers, responseBody);
}
49. JSON Serialization and Deserialization
| Operation |
Direction |
Common Function |
| Decoding |
JSON String → Dart data |
jsonDecode() |
| Deserialization |
JSON Map → Dart Model |
fromJson() |
| Encoding |
Dart data → JSON String |
jsonEncode() |
| Serialization |
Dart Model → JSON-compatible data |
toJson() |
50. Common Mistakes While Processing JSON
- Forgetting to import
dart:convert.
- Calling
jsonDecode() on invalid JSON.
- Assuming every API response is a JSON object.
- Assuming an array response is a map.
- Not checking the HTTP status code.
- Using non-nullable fields when the API can return null.
- Using incorrect JSON field names.
- Ignoring nested JSON structures.
- Not handling API errors.
- Putting all JSON parsing logic directly inside widgets.
- Parsing very large JSON payloads synchronously when background processing is more appropriate.
51. Best Practices for Reading JSON Responses
- Always check the HTTP status code.
- Decode JSON using
jsonDecode().
- Use model classes for reusable API data.
- Create
fromJson() methods for deserialization.
- Create
toJson() methods when data must be sent back to an API.
- Handle nullable and optional fields.
- Handle malformed JSON and network errors.
- Keep API communication outside the UI when possible.
- Use typed collections for better readability.
- Consider background parsing for sufficiently large JSON payloads.
- Test model serialization and deserialization.
52. Complete Example: Reading JSON from an API
import 'dart:convert';
import 'package:http/http.dart' as http;
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
Future<User> fetchUser() async {
final response = await http.get(
Uri.parse('https://example.com/api/user/1'),
headers: {
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = jsonDecode(response.body) as Map;
return User.fromJson(data);
} on FormatException {
throw Exception('Server returned invalid JSON');
}
}
throw Exception(
'Failed to load user. Status: ${response.statusCode}',
);
}
53. Complete Example: Displaying the Processed JSON in Flutter
import 'package:flutter/material.dart';
class UserScreen extends StatelessWidget {
const UserScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User'),
),
body: FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text(
'Error: ${snapshot.error}',
),
);
}
if (!snapshot.hasData) {
return const Center(
child: Text('No user data'),
);
}
final user = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('ID: ${user.id}'),
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
),
);
},
),
);
}
}
54. JSON Processing Architecture Example
API Server
↓
HTTP GET Request
↓
http.Response
↓
Check statusCode
↓
response.body
↓
jsonDecode()
↓
Map
↓
User.fromJson()
↓
User Object
↓
FutureBuilder
↓
Flutter Widget
55. Quick Revision
| Concept |
Purpose |
dart:convert |
Provides JSON conversion utilities |
jsonDecode() |
Converts JSON text into Dart data |
jsonEncode() |
Converts Dart data into JSON text |
fromJson() |
Creates a Dart model from JSON data |
toJson() |
Converts a Dart model into JSON-compatible data |
response.body |
Contains the HTTP response body |
response.statusCode |
Indicates the HTTP response status |
FutureBuilder |
Displays asynchronous API results in Flutter UI |
56. Interview Questions
- What is JSON?
- Why is JSON commonly used with Flutter APIs?
- Which Dart library is used for JSON encoding and decoding?
- What does
jsonDecode() do?
- What does
jsonEncode() do?
- What happens when a JSON object is decoded?
- What happens when a JSON array is decoded?
- What is the purpose of
fromJson()?
- What is the purpose of
toJson()?
- Why should HTTP status codes be checked before processing a response?
- How do you process a JSON list in Flutter?
- How do you process nested JSON?
- How do you handle nullable JSON fields?
- What exception can occur when JSON is invalid?
- Why are model classes useful for API responses?
- How can large JSON responses be processed without blocking the UI?
57. Practice Exercise
Create a Flutter application that reads the following JSON response and displays the users in a ListView.
[
{
"id": 1,
"name": "Rahul",
"email": "[email protected]"
},
{
"id": 2,
"name": "Priya",
"email": "[email protected]"
},
{
"id": 3,
"name": "Amit",
"email": "[email protected]"
}
]
Tasks
- Create a
User model.
- Create a
fromJson() constructor.
- Decode the JSON using
jsonDecode().
- Convert every JSON object into a
User.
- Store the users in a
List.
- Display the users using
ListView.builder.
- Add error handling for invalid JSON.
58. Official Learning Resources
59. Learn Flutter with JustAcademy
For structured Flutter learning, explore the following resources:
60. Summary
Reading and processing JSON responses is an essential skill for Flutter application development. A typical API workflow starts with an HTTP request, receives a response, checks the status code, reads the response body, decodes JSON using jsonDecode(), and converts the decoded data into Dart model objects using fromJson(). For sending data to an API, Dart uses jsonEncode() and model classes can provide toJson(). For production applications, proper error handling, nullable fields, nested models, typed collections, and appropriate JSON parsing architecture help make API-driven Flutter applications easier to maintain and more reliable.